// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/driver.h>
#include <ddk/platform-defs.h>
#include <ddk/protocol/mailbox.h>
#include <ddk/protocol/platform/device.h>
#include <fbl/algorithm.h>
#include <fbl/auto_lock.h>
#include <fbl/unique_ptr.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <zircon/assert.h>

#include "aml-scpi.h"

namespace scpi {

zx_status_t AmlSCPI::GetMailbox(uint32_t cmd, mailbox_type_t* mailbox) {
  if (!mailbox || !VALID_CMD(cmd)) {
    return ZX_ERR_INVALID_ARGS;
  }

  for (uint32_t i = 0; i < countof(aml_low_priority_cmds); i++) {
    if (cmd == aml_low_priority_cmds[i]) {
      *mailbox = MAILBOX_TYPE_AP_NS_LOW_PRIORITY_MAILBOX;
      return ZX_OK;
    }
  }

  for (uint32_t i = 0; i < countof(aml_high_priority_cmds); i++) {
    if (cmd == aml_high_priority_cmds[i]) {
      *mailbox = MAILBOX_TYPE_AP_NS_HIGH_PRIORITY_MAILBOX;
      return ZX_OK;
    }
  }

  for (uint32_t i = 0; i < countof(aml_secure_cmds); i++) {
    if (cmd == aml_secure_cmds[i]) {
      *mailbox = MAILBOX_TYPE_AP_SECURE_MAILBOX;
      return ZX_OK;
    }
  }
  *mailbox = MAILBOX_TYPE_INVALID_MAILBOX;
  return ZX_ERR_NOT_FOUND;
}

zx_status_t AmlSCPI::ExecuteCommand(void* rx_buf, size_t rx_size, void* tx_buf, size_t tx_size,
                                    uint32_t cmd, uint32_t client_id) {
  uint32_t mailbox_status = 0;
  mailbox_data_buf_t mdata;
  mdata.cmd = PACK_SCPI_CMD(cmd, client_id, 0);
  mdata.tx_buffer = tx_buf;
  mdata.tx_size = tx_size;

  mailbox_channel_t channel;
  zx_status_t status = GetMailbox(cmd, &channel.mailbox);
  if (status != ZX_OK) {
    SCPI_ERROR("aml_scpi_get_mailbox failed - error status %d\n", status);
    return status;
  }

  channel.rx_buffer = rx_buf;
  channel.rx_size = rx_size;

  status = mailbox_.SendCommand(&channel, &mdata);
  if (rx_buf) {
    mailbox_status = *(uint32_t*)(rx_buf);
  }
  if (status != ZX_OK || mailbox_status != 0) {
    SCPI_ERROR("mailbox_send_command failed - error status %d\n", status);
    return status;
  }
  return ZX_OK;
}

zx_status_t AmlSCPI::ScpiGetDvfsIdx(uint8_t power_domain, uint16_t* idx) {
  struct {
    uint32_t status;
    uint8_t idx;
    uint8_t padding[3];
  } __PACKED aml_dvfs_idx_info;

  if (!idx || power_domain >= fuchsia_hardware_thermal_MAX_DVFS_DOMAINS) {
    return ZX_ERR_INVALID_ARGS;
  }

  zx_status_t status = ExecuteCommand(&aml_dvfs_idx_info, sizeof(aml_dvfs_idx_info), &power_domain,
                                      sizeof(power_domain), SCPI_CMD_GET_DVFS, SCPI_CL_DVFS);
  if (status != ZX_OK) {
    return status;
  }

  *idx = aml_dvfs_idx_info.idx;

  SCPI_INFO("Current Operation point %x\n", aml_dvfs_idx_info.idx);
  return ZX_OK;
}

zx_status_t AmlSCPI::ScpiSetDvfsIdx(uint8_t power_domain, uint16_t idx) {
  struct {
    uint8_t power_domain;
    uint16_t idx;
    uint8_t padding;
  } __PACKED aml_dvfs_idx_info;

  if (power_domain >= fuchsia_hardware_thermal_MAX_DVFS_DOMAINS) {
    return ZX_ERR_INVALID_ARGS;
  }

  aml_dvfs_idx_info.power_domain = power_domain;
  aml_dvfs_idx_info.idx = idx;

  SCPI_INFO("OPP index for cluster %d to %d\n", power_domain, idx);
  return ExecuteCommand(NULL, 0, &aml_dvfs_idx_info, sizeof(aml_dvfs_idx_info), SCPI_CMD_SET_DVFS,
                        SCPI_CL_DVFS);
}

zx_status_t AmlSCPI::ScpiGetDvfsInfo(uint8_t power_domain, scpi_opp_t* out_opps) {
  zx_status_t status;
  struct {
    uint32_t status;
    uint8_t reserved;
    uint8_t operating_points;
    uint16_t latency;
    scpi_opp_entry_t opp[fuchsia_hardware_thermal_MAX_DVFS_OPPS];
  } __PACKED aml_dvfs_info;

  if (!out_opps || power_domain >= fuchsia_hardware_thermal_MAX_DVFS_DOMAINS) {
    return ZX_ERR_INVALID_ARGS;
  }

  fbl::AutoLock mailbox_lock(&lock_);

  // dvfs info already populated
  if (scpi_opp[power_domain]) {
    memcpy(out_opps, scpi_opp[power_domain], sizeof(scpi_opp_t));
    return ZX_OK;
  }

  uint32_t power_domain_scpi = 0;
  status = ExecuteCommand(&aml_dvfs_info, sizeof(aml_dvfs_info), &power_domain_scpi,
                          sizeof(power_domain_scpi), SCPI_CMD_GET_DVFS_INFO, SCPI_CL_DVFS);
  if (status != ZX_OK) {
    return status;
  }

  ZX_DEBUG_ASSERT(power_domain_scpi <= UINT8_MAX);
  power_domain = static_cast<uint8_t>(power_domain_scpi);

  out_opps->count = aml_dvfs_info.operating_points;
  out_opps->latency = aml_dvfs_info.latency;

  if (out_opps->count > fuchsia_hardware_thermal_MAX_DVFS_OPPS) {
    SCPI_ERROR("Number of operating_points greater than fuchsia_hardware_thermal_MAX_DVFS_OPPS\n");
    status = ZX_ERR_INVALID_ARGS;
    return status;
  }

  zxlogf(INFO, "Cluster %u details\n", power_domain);
  zxlogf(INFO, "Number of operating_points %u\n", aml_dvfs_info.operating_points);
  zxlogf(INFO, "latency %u uS\n", aml_dvfs_info.latency);

  for (uint32_t i = 0; i < out_opps->count; i++) {
    out_opps->opp[i].freq_hz = aml_dvfs_info.opp[i].freq_hz;
    out_opps->opp[i].volt_uv = aml_dvfs_info.opp[i].volt_uv;
    zxlogf(INFO, "Operating point %d - ", i);
    zxlogf(INFO, "Freq %.4f Ghz ", (out_opps->opp[i].freq_hz) / (double)1000000000);
    zxlogf(INFO, "Voltage %.4f mV\n", (out_opps->opp[i].volt_uv) / (double)1000);
  }

  scpi_opp[power_domain] = static_cast<scpi_opp_t*>(calloc(1, sizeof(scpi_opp_t)));
  if (!scpi_opp[power_domain]) {
    status = ZX_ERR_NO_MEMORY;
    return status;
  }

  memcpy(scpi_opp[power_domain], out_opps, sizeof(scpi_opp_t));
  return ZX_OK;
}

zx_status_t AmlSCPI::ScpiGetSensorValue(uint32_t sensor_id, uint32_t* sensor_value) {
  struct {
    uint32_t status;
    uint16_t sensor_value;
    uint16_t padding;
  } __PACKED aml_sensor_val;

  if (!sensor_value) {
    return ZX_ERR_INVALID_ARGS;
  }

  zx_status_t status = ExecuteCommand(&aml_sensor_val, sizeof(aml_sensor_val), &sensor_id,
                                      sizeof(sensor_id), SCPI_CMD_SENSOR_VALUE, SCPI_CL_THERMAL);
  if (status != ZX_OK) {
    return status;
  }
  *sensor_value = aml_sensor_val.sensor_value;
  return ZX_OK;
}

zx_status_t AmlSCPI::ScpiGetSensor(const char* name, uint32_t* sensor_value) {
  struct {
    uint32_t status;
    uint16_t num_sensors;
    uint16_t padding;
  } __PACKED aml_sensor_cap;

  struct {
    uint32_t status;
    uint16_t sensor;
    uint8_t sensor_class;
    uint8_t trigger;
    char sensor_name[20];
  } __PACKED aml_sensor_info;

  if (sensor_value == NULL) {
    return ZX_ERR_INVALID_ARGS;
  }

  // First let's find information about all sensors
  zx_status_t status = ExecuteCommand(&aml_sensor_cap, sizeof(aml_sensor_cap), NULL, 0,
                                      SCPI_CMD_SENSOR_CAPABILITIES, SCPI_CL_THERMAL);
  if (status != ZX_OK) {
    return status;
  }

  // Loop through all the sensors
  for (uint32_t sensor_id = 0; sensor_id < aml_sensor_cap.num_sensors; sensor_id++) {
    status = ExecuteCommand(&aml_sensor_info, sizeof(aml_sensor_info), &sensor_id,
                            sizeof(sensor_id), SCPI_CMD_SENSOR_INFO, SCPI_CL_THERMAL);
    if (status != ZX_OK) {
      return status;
    }
    if (!strncmp(name, aml_sensor_info.sensor_name, sizeof(aml_sensor_info.sensor_name))) {
      *sensor_value = sensor_id;
      break;
    }
  }
  return ZX_OK;
}

void AmlSCPI::DdkUnbind() { DdkRemove(); }

void AmlSCPI::DdkRelease() { delete this; }

zx_status_t AmlSCPI::Bind() {
  zx_device_prop_t props[] = {
      {BIND_PLATFORM_DEV_VID, 0, PDEV_VID_AMLOGIC},
      {BIND_PLATFORM_DEV_DID, 0, PDEV_DID_AMLOGIC_SCPI},
  };

  return DdkAdd("aml-scpi", 0, props, fbl::count_of(props));
}

zx_status_t AmlSCPI::Create(zx_device_t* parent) {
  fbl::AllocChecker ac;
  auto scpi_device = fbl::make_unique_checked<AmlSCPI>(&ac, parent);
  if (!ac.check()) {
    return ZX_ERR_NO_MEMORY;
  }

  zx_status_t status = ZX_ERR_INTERNAL;
  // Get ZX_PROTOCOL_MAILBOX protocol.
  if (!scpi_device->mailbox_.is_valid()) {
    zxlogf(ERROR, "dwmac: could not obtain ZX_PROTOCOL_MAILBOX protocol: %d\n", status);
    return status;
  }

  mtx_init(&scpi_device->lock_, mtx_plain);

  status = scpi_device->Bind();
  if (status != ZX_OK) {
    zxlogf(ERROR, "aml-scpi driver failed to get added: %d\n", status);
    return status;
  } else {
    zxlogf(INFO, "aml-scpi driver added\n");
  }

  // scpi_device intentionally leaked as it is now held by DevMgr
  __UNUSED auto ptr = scpi_device.release();

  return ZX_OK;
}

zx_status_t aml_scpi_bind(void* ctx, zx_device_t* parent) { return scpi::AmlSCPI::Create(parent); }

static constexpr zx_driver_ops_t driver_ops = []() {
  zx_driver_ops_t ops = {};
  ops.version = DRIVER_OPS_VERSION;
  ops.bind = aml_scpi_bind;
  return ops;
}();

}  // namespace scpi

// clang-format off
ZIRCON_DRIVER_BEGIN(aml_scpi, scpi::driver_ops, "zircon", "0.1", 2)
    BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_AMLOGIC),
    BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_DID, PDEV_DID_AMLOGIC_MAILBOX),
ZIRCON_DRIVER_END(aml_scpi)
